package it.unibo.refolding.pico;

import it.unibo.refolding.common.CommonThings;
import it.unibo.refolding.common.MyRuntimeException;
import it.unibo.refolding.common.Sleeper;
import it.unibo.refolding.pico.FileMovingWatcher.FileContentAndTargetFile;
import it.unibo.refolding.pico.PicoDataAnalyser.OffsetAndDeflections;

import java.awt.Shape;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;

public class RefoldingUtil {

  private static final Logger logger = Logger.getLogger(RefoldingUtil.class.getSimpleName());

  public static int minPeaksForInterestingFetch(
    double fetchMoleculeRampSizeM, double pushingM, double clRangeStart, double clRangeEnd,
    double linkerM, double linkerPerModuleM) {
    double rampM = fetchMoleculeRampSizeM - pushingM;
    return minPeaks(rampM, clRangeStart, clRangeEnd, linkerM, linkerPerModuleM);
  }

  public static int minPeaks(
    double rampM, double clRangeStart, double clRangeEnd, double linkerM, double linkerPerModuleM) {
    int minPeaks = (int) ((rampM - linkerM) / ((clRangeStart + clRangeEnd) / 2 + linkerPerModuleM)) - 1;
    Level level = Level.FINER;
    if (logger.isLoggable(level)) {
      logger.log(level, String.format(
        Locale.ROOT, "rampM=%s clRangeStart=%s clRangeEnd=%s minPeaks=%s", rampM, clRangeStart,
        clRangeEnd, minPeaks));
    }
    return Math.max(minPeaks, 1);
  }

  public static boolean inadvertentlyPushing(
    RefoldingResult refoldingResult, double retractionCpZLsb) {
    boolean pushing = refoldingResult.retraction().sharedZArray()[0] >= retractionCpZLsb;
    if (pushing) {
      logger.warning(String.format(
        Locale.ROOT,
        "Pushing fullRefoldingPicoData.retraction.sharedZArray()[0]=%s latest.retractionContactPoint.zLsb=%s. Did we drift?",
        refoldingResult.retraction().sharedZArray()[0], retractionCpZLsb));
    }
    return pushing;
  }

  public static class RefoldingCurve {
    public final boolean[] peaksIndicator;
    public final List<Shape> shapes;

    public RefoldingCurve(boolean[] peaksIndicator, List<Shape> shapes) {
      this.peaksIndicator = peaksIndicator;
      this.shapes = Preconditions.checkNotNull(shapes);
    }
  }

  public static RefoldingCurve analyseRefoldingCurve(
    RefoldingResult refoldingResult, double minPeakForceRelativeToEmpiricalBaselineN,
    double medianAbsoluteDeviationLowerBoundN, double leftMostlyHigherWindowSizeM,
    double leftMostlyHigherFraction, double leftMostlyHigherN,
    double rightMuchHigherThanAllN, double rightMuchHigherN, double atMostOnePeakWindowM,
    double baselineSlopeZLsbDefLsb, boolean draw, double svgDefScale) {
    // Any fake retraction point will do it.
    ZLsbDeflectionLsbPoint fakeRetractionContactPoint = new ZLsbDeflectionLsbPoint(0, 0);
    OffsetAndDeflections offsetAndDeflections = PicoDataAnalyser.createOffsetAndDeflections(
      fakeRetractionContactPoint.linearFunctionWithSlope(baselineSlopeZLsbDefLsb),
      refoldingResult.retraction(), 0);
    boolean[] peaksIndicator = new boolean[refoldingResult.retraction().timeInstantCount()];
    Arrays.fill(peaksIndicator, true);
    double empiricalBaseline = PicoDataAnalyser.findEmpiricalBasalLevel(
      offsetAndDeflections.horizontalDeflectionAfterContactPoint,
      PicoDataAnalyser.defaultEmpiricalBaselinePercentile);
    PicoDataAnalyser.findPeaksInvariantUponTranslationOfDeflectionData(
      offsetAndDeflections.horizontalDeflectionAfterContactPoint,
      refoldingResult.deflectionLsbToNewtonsFactor(),
      refoldingResult.actualRetractionStepSizeMetres(), empiricalBaseline,
      minPeakForceRelativeToEmpiricalBaselineN, medianAbsoluteDeviationLowerBoundN,
      leftMostlyHigherWindowSizeM, leftMostlyHigherFraction, leftMostlyHigherN,
      rightMuchHigherThanAllN, rightMuchHigherN, atMostOnePeakWindowM,
      peaksIndicator);
    final RefoldingCurve refoldingCurve;
    if (draw) {
      int ellipseHeight = 10;
      double ellipseWidth = 10;
      List<Shape> peakRelatedShapes = new ArrayList<>();
      for (int i = peaksIndicator.length; --i >= 0;) {
        if (peaksIndicator[i]) {
          short centerX = refoldingResult.retraction().z(i);
          short centerY = refoldingResult.retraction().deflectionAtIndex(i);
          Shape ellipse = new Ellipse2D.Double(centerX - ellipseWidth / 2, centerY * svgDefScale - ellipseHeight
            / 2, ellipseWidth, ellipseHeight);
          peakRelatedShapes.add(ellipse);
        }
      }
      Line2D empiricalBaselineLine = PicoShapes.drawEmpiricalBaseline(
        refoldingResult.retraction(), fakeRetractionContactPoint, baselineSlopeZLsbDefLsb,
        empiricalBaseline, svgDefScale);
      List<Shape> shapes = new ArrayList<>();
      shapes.addAll(peakRelatedShapes);
      shapes.add(empiricalBaselineLine);
      refoldingCurve = new RefoldingCurve(peaksIndicator, shapes);
    } else {
      refoldingCurve = new RefoldingCurve(peaksIndicator, ImmutableList.<Shape>of());
    }
    return refoldingCurve;
  }

  public static void checkCaptureDirIsEmpty(Path captureDir) {
    try (DirectoryStream<Path> stream = Files.newDirectoryStream(captureDir)) {
      if (stream.iterator().hasNext()) {
        throw new MyRuntimeException("Capture directory must exist and be empty: " + captureDir);
      }
    } catch (IOException e) {
      throw new MyRuntimeException(e.getMessage(), e);
    }
  }

  public static TimedFileContentAndTargetFile executeScript(
    String prefix, String suffix, double timeS, Sleeper nanoDriverSleeper, boolean resetAfterCapture, NanoDriver nanoDriver,
    FileMovingWatcher fileMovingWatcher) {
    long startNanoTime = System.nanoTime();
    nanoDriver.runScript();
    CommonThings.sleepS(timeS, nanoDriverSleeper);
    return afterScriptRun(
      startNanoTime, prefix, suffix, resetAfterCapture, nanoDriver, fileMovingWatcher);
  }

  public static TimedFileContentAndTargetFile afterScriptRun(
    long startNanoTime, String prefix, String suffix, boolean resetAfterCapture,
    NanoDriver nanoDriver, FileMovingWatcher fileMovingWatcher) {
    nanoDriver.waitWhileStartOrPauseIsNotRed();
    if (resetAfterCapture) {
      nanoDriver.captureButtonResetStripChart();
    } else {
      nanoDriver.captureButton();
    }
    long endNanoTime1 = System.nanoTime();
    FileContentAndTargetFile latestFileContentAndTargetFile = fileMovingWatcher.latestFileContentAndTargetFile(
      prefix, suffix, true);
    long endNanoTime2 = System.nanoTime();
    /*
     * Wait for the piezo to return to the starting position. Not sure how
     * necessary it is though.
     */
    if (!resetAfterCapture) {
      nanoDriver.quitStripChart();
    }
    return new TimedFileContentAndTargetFile(
      latestFileContentAndTargetFile, (endNanoTime1 - startNanoTime) / 1e9,
      (endNanoTime2 - startNanoTime) / 1e9);
  }
}
